[id].tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. import { Image } from 'expo-image';
  2. import { useLocalSearchParams, useRouter } from 'expo-router';
  3. import React, { useCallback, useEffect, useRef, useState } from 'react';
  4. import {
  5. ActivityIndicator,
  6. Dimensions,
  7. ImageBackground,
  8. ScrollView,
  9. StatusBar,
  10. StyleSheet,
  11. Text,
  12. TouchableOpacity,
  13. View,
  14. } from 'react-native';
  15. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  16. import { KefuPopup, KefuPopupRef } from '@/components/mine/KefuPopup';
  17. import { CheckoutModal } from '@/components/product/CheckoutModal';
  18. import { Images } from '@/constants/images';
  19. import { getGoodsDetail, GoodsDetail, previewSubmit } from '@/services/mall';
  20. const { width: SCREEN_WIDTH } = Dimensions.get('window');
  21. export default function ProductDetailScreen() {
  22. const { id, subjectId } = useLocalSearchParams<{ id: string; subjectId?: string }>();
  23. const router = useRouter();
  24. const insets = useSafeAreaInsets();
  25. const checkoutRef = useRef<any>(null);
  26. const kefuRef = useRef<KefuPopupRef>(null);
  27. const [loading, setLoading] = useState(true);
  28. const [data, setData] = useState<GoodsDetail | null>(null);
  29. // 加载商品详情
  30. const loadData = useCallback(async () => {
  31. if (!id) return;
  32. setLoading(true);
  33. try {
  34. const detail = await getGoodsDetail(id, subjectId);
  35. setData(detail);
  36. } catch (error) {
  37. console.error('加载商品详情失败:', error);
  38. }
  39. setLoading(false);
  40. }, [id, subjectId]);
  41. useEffect(() => {
  42. loadData();
  43. }, [loadData]);
  44. // 显示结算弹窗
  45. const showCheckout = async () => {
  46. if (!data) return;
  47. try {
  48. const preview = await previewSubmit({
  49. goodsId: id!,
  50. quantity: 1,
  51. subjectId,
  52. });
  53. if (preview) {
  54. checkoutRef.current?.show(preview);
  55. }
  56. } catch (error) {
  57. console.error('预提交失败:', error);
  58. }
  59. };
  60. // 返回
  61. const goBack = () => {
  62. router.back();
  63. };
  64. if (loading) {
  65. return (
  66. <View style={styles.loadingContainer}>
  67. <ActivityIndicator size="large" color="#fff" />
  68. </View>
  69. );
  70. }
  71. if (!data) {
  72. return (
  73. <View style={styles.loadingContainer}>
  74. <Text style={styles.errorText}>商品不存在</Text>
  75. </View>
  76. );
  77. }
  78. return (
  79. <View style={styles.container}>
  80. <StatusBar barStyle="light-content" />
  81. <ImageBackground
  82. source={{ uri: Images.mine.kaixinMineBg }}
  83. style={styles.background}
  84. resizeMode="cover"
  85. >
  86. {/* 顶部导航 */}
  87. <View style={[styles.header, { paddingTop: insets.top }]}>
  88. <TouchableOpacity style={styles.backBtn} onPress={goBack}>
  89. <Text style={styles.backText}>←</Text>
  90. </TouchableOpacity>
  91. <Text style={styles.headerTitle}>商品详情</Text>
  92. <View style={styles.placeholder} />
  93. </View>
  94. <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
  95. {/* 商品图片 */}
  96. <View style={styles.imageWrapper}>
  97. <Image source={data.spu.cover} style={styles.coverImage} contentFit="cover" />
  98. </View>
  99. {/* 商品信息 */}
  100. <ImageBackground
  101. source={{ uri: Images.common.itemBg }}
  102. style={styles.infoSection}
  103. resizeMode="stretch"
  104. >
  105. <View style={styles.priceRow}>
  106. <Text style={styles.currency}>¥</Text>
  107. <Text style={styles.price}>{data.subjectPrice || data.price}</Text>
  108. {data.sellType === 2 && (
  109. <View style={styles.presellTag}>
  110. <Text style={styles.presellText}>预售</Text>
  111. </View>
  112. )}
  113. </View>
  114. <Text style={styles.name}>{data.spu.name}</Text>
  115. </ImageBackground>
  116. {/* 商品详情图片 */}
  117. {data.spu.images && data.spu.images.length > 0 && (
  118. <ImageBackground
  119. source={{ uri: Images.common.itemBg }}
  120. style={styles.detailSection}
  121. resizeMode="stretch"
  122. >
  123. <Text style={styles.sectionTitle}>商品详情</Text>
  124. {data.spu.images.map((img, index) => (
  125. <Image key={index} source={img} style={styles.detailImage} contentFit="contain" />
  126. ))}
  127. </ImageBackground>
  128. )}
  129. {/* 推荐商品 */}
  130. {data.recommendedMallGoods && data.recommendedMallGoods.length > 0 && (
  131. <ImageBackground
  132. source={{ uri: Images.common.itemBg }}
  133. style={styles.recommendSection}
  134. resizeMode="stretch"
  135. >
  136. <Text style={styles.sectionTitle}>推荐商品</Text>
  137. <ScrollView horizontal showsHorizontalScrollIndicator={false}>
  138. {data.recommendedMallGoods.map((item) => (
  139. <TouchableOpacity
  140. key={item.id}
  141. style={styles.recommendItem}
  142. onPress={() => router.push(`/product/${item.id}` as any)}
  143. >
  144. <Image source={item.cover} style={styles.recommendImage} contentFit="cover" />
  145. <Text style={styles.recommendName} numberOfLines={1}>{item.name}</Text>
  146. <Text style={styles.recommendPrice}>¥{item.price}</Text>
  147. </TouchableOpacity>
  148. ))}
  149. </ScrollView>
  150. </ImageBackground>
  151. )}
  152. <View style={styles.bottomSpace} />
  153. </ScrollView>
  154. {/* 底部购买栏 */}
  155. <View style={[styles.bottomBar, { paddingBottom: insets.bottom + 10 }]}>
  156. <TouchableOpacity style={styles.serviceBtn} onPress={() => kefuRef.current?.open()}>
  157. <Text style={styles.serviceBtnText}>客服</Text>
  158. </TouchableOpacity>
  159. <TouchableOpacity style={styles.buyBtn} onPress={showCheckout} activeOpacity={0.8}>
  160. <ImageBackground
  161. source={{ uri: Images.common.loginBtn }}
  162. style={styles.buyBtnBg}
  163. resizeMode="stretch"
  164. >
  165. <Text style={styles.buyBtnText}>
  166. {data.sellType === 2 ? '支付定金' : '立即购买'}
  167. </Text>
  168. </ImageBackground>
  169. </TouchableOpacity>
  170. </View>
  171. </ImageBackground>
  172. {/* 结算弹窗 */}
  173. <CheckoutModal
  174. ref={checkoutRef}
  175. data={data}
  176. goodsId={id!}
  177. subjectId={subjectId}
  178. />
  179. <KefuPopup ref={kefuRef} />
  180. </View>
  181. );
  182. }
  183. const styles = StyleSheet.create({
  184. container: {
  185. flex: 1,
  186. backgroundColor: '#1a1a2e',
  187. },
  188. background: {
  189. flex: 1,
  190. },
  191. loadingContainer: {
  192. flex: 1,
  193. backgroundColor: '#1a1a2e',
  194. justifyContent: 'center',
  195. alignItems: 'center',
  196. },
  197. errorText: {
  198. color: '#999',
  199. fontSize: 16,
  200. },
  201. header: {
  202. flexDirection: 'row',
  203. alignItems: 'center',
  204. justifyContent: 'space-between',
  205. paddingHorizontal: 15,
  206. paddingBottom: 10,
  207. },
  208. backBtn: {
  209. width: 40,
  210. height: 40,
  211. justifyContent: 'center',
  212. alignItems: 'center',
  213. },
  214. backText: {
  215. color: '#fff',
  216. fontSize: 24,
  217. },
  218. headerTitle: {
  219. color: '#fff',
  220. fontSize: 18,
  221. fontWeight: '600',
  222. },
  223. placeholder: {
  224. width: 40,
  225. },
  226. scrollView: {
  227. flex: 1,
  228. },
  229. imageWrapper: {
  230. width: SCREEN_WIDTH,
  231. height: SCREEN_WIDTH,
  232. backgroundColor: '#fff',
  233. },
  234. coverImage: {
  235. width: '100%',
  236. height: '100%',
  237. },
  238. infoSection: {
  239. padding: 15,
  240. marginHorizontal: 10,
  241. marginTop: 10,
  242. borderRadius: 12,
  243. overflow: 'hidden',
  244. },
  245. priceRow: {
  246. flexDirection: 'row',
  247. alignItems: 'baseline',
  248. },
  249. currency: {
  250. color: '#ff6b00',
  251. fontSize: 14,
  252. },
  253. price: {
  254. color: '#ff6b00',
  255. fontSize: 28,
  256. fontWeight: 'bold',
  257. },
  258. presellTag: {
  259. backgroundColor: '#8b3dff',
  260. borderRadius: 12,
  261. paddingHorizontal: 10,
  262. paddingVertical: 3,
  263. marginLeft: 10,
  264. },
  265. presellText: {
  266. color: '#fff',
  267. fontSize: 12,
  268. },
  269. name: {
  270. color: '#333',
  271. fontSize: 16,
  272. marginTop: 10,
  273. lineHeight: 22,
  274. },
  275. detailSection: {
  276. marginTop: 10,
  277. marginHorizontal: 10,
  278. padding: 15,
  279. borderRadius: 12,
  280. overflow: 'hidden',
  281. },
  282. sectionTitle: {
  283. color: '#333',
  284. fontSize: 16,
  285. fontWeight: '600',
  286. marginBottom: 15,
  287. },
  288. detailImage: {
  289. width: SCREEN_WIDTH - 50,
  290. height: 300,
  291. marginBottom: 10,
  292. },
  293. recommendSection: {
  294. marginTop: 10,
  295. marginHorizontal: 10,
  296. padding: 15,
  297. borderRadius: 12,
  298. overflow: 'hidden',
  299. },
  300. recommendItem: {
  301. width: 120,
  302. marginRight: 10,
  303. },
  304. recommendImage: {
  305. width: 120,
  306. height: 120,
  307. borderRadius: 8,
  308. backgroundColor: '#fff',
  309. },
  310. recommendName: {
  311. color: '#333',
  312. fontSize: 12,
  313. marginTop: 8,
  314. },
  315. recommendPrice: {
  316. color: '#ff6b00',
  317. fontSize: 14,
  318. fontWeight: '600',
  319. marginTop: 4,
  320. },
  321. bottomSpace: {
  322. height: 100,
  323. },
  324. bottomBar: {
  325. position: 'absolute',
  326. bottom: 0,
  327. left: 0,
  328. right: 0,
  329. flexDirection: 'row',
  330. alignItems: 'center',
  331. paddingHorizontal: 15,
  332. paddingTop: 10,
  333. backgroundColor: 'rgba(0,0,0,0.3)',
  334. },
  335. serviceBtn: {
  336. paddingHorizontal: 25,
  337. paddingVertical: 12,
  338. backgroundColor: 'rgba(255,255,255,0.9)',
  339. borderRadius: 25,
  340. },
  341. serviceBtnText: {
  342. color: '#333',
  343. fontSize: 14,
  344. },
  345. buyBtn: {
  346. flex: 1,
  347. marginLeft: 15,
  348. height: 45,
  349. overflow: 'hidden',
  350. },
  351. buyBtnBg: {
  352. width: '100%',
  353. height: '100%',
  354. justifyContent: 'center',
  355. alignItems: 'center',
  356. },
  357. buyBtnText: {
  358. color: '#fff',
  359. fontSize: 16,
  360. fontWeight: '600',
  361. },
  362. });